define(function(){
    /**
     * http://github.com/valums/file-uploader
     * 
     * Multiple file upload component with progress-bar, drag-and-drop. 
     * © 2010 Andrew Valums ( andrew(at)valums.com ) 
     * 
     * Licensed under GNU GPL 2 or later and GNU LGPL 2 or later, see license.txt.
     */    

    //
    // Helper functions
    //

    var qq = qq || {};

    /**
     * Adds all missing properties from second obj to first obj
     */ 
    qq.extend = function(first, second){
        for (var prop in second){
            first[prop] = second[prop];
        }
    };  

    /**
     * Searches for a given element in the array, returns -1 if it is not present.
     * @param {Number} [from] The index at which to begin the search
     */
    qq.indexOf = function(arr, elt, from){
        if (arr.indexOf) return arr.indexOf(elt, from);
        
        from = from || 0;
        var len = arr.length;    
        
        if (from < 0) from += len;  

        for (; from < len; from++){  
            if (from in arr && arr[from] === elt){  
                return from;
            }
        }  
        return -1;  
    }; 
        
    qq.getUniqueId = (function(){
        var id = 0;
        return function(){ return id++; };
    })();

    //
    // Events

    qq.attach = function(element, type, fn){
        if (element.addEventListener){
            element.addEventListener(type, fn, false);
        } else if (element.attachEvent){
            element.attachEvent('on' + type, fn);
        }
    };
    qq.detach = function(element, type, fn){
        if (element.removeEventListener){
            element.removeEventListener(type, fn, false);
        } else if (element.attachEvent){
            element.detachEvent('on' + type, fn);
        }
    };

    qq.preventDefault = function(e){
        if (e.preventDefault){
            e.preventDefault();
        } else{
            e.returnValue = false;
        }
    };

    //
    // Node manipulations

    /**
     * Insert node a before node b.
     */
    qq.insertBefore = function(a, b){
        b.parentNode.insertBefore(a, b);
    };
    qq.remove = function(element){
        element.parentNode.removeChild(element);
    };

    qq.contains = function(parent, descendant){       
        // compareposition returns false in this case
        if (parent == descendant) return true;
        
        if (parent.contains){
            return parent.contains(descendant);
        } else {
            return !!(descendant.compareDocumentPosition(parent) & 8);
        }
    };

    /**
     * Creates and returns element from html string
     * Uses innerHTML to create an element
     */
    qq.toElement = (function(){
        var div = document.createElement('div');
        return function(html){
            div.innerHTML = html;
            var element = div.firstChild;
            div.removeChild(element);
            return element;
        };
    })();

    //
    // Node properties and attributes

    /**
     * Sets styles for an element.
     * Fixes opacity in IE6-8.
     */
    qq.css = function(element, styles){
        if (styles.opacity != null){
            if (typeof element.style.opacity != 'string' && typeof(element.filters) != 'undefined'){
                styles.filter = 'alpha(opacity=' + Math.round(100 * styles.opacity) + ')';
            }
        }
        qq.extend(element.style, styles);
    };
    qq.hasClass = function(element, name){
        var re = new RegExp('(^| )' + name + '( |$)');
        return re.test(element.className);
    };
    qq.addClass = function(element, name){
        if (!qq.hasClass(element, name)){
            element.className += ' ' + name;
        }
    };
    qq.removeClass = function(element, name){
        var re = new RegExp('(^| )' + name + '( |$)');
        element.className = element.className.replace(re, ' ').replace(/^\s+|\s+$/g, "");
    };
    qq.setText = function(element, text){
        element.innerText = text;
        element.textContent = text;
    };

    //
    // Selecting elements

    qq.children = function(element){
        var children = [],
        child = element.firstChild;

        while (child){
            if (child.nodeType == 1){
                children.push(child);
            }
            child = child.nextSibling;
        }

        return children;
    };

    qq.getByClass = function(element, className){
        if (element.querySelectorAll){
            return element.querySelectorAll('.' + className);
        }

        var result = [];
        var candidates = element.getElementsByTagName("*");
        var len = candidates.length;

        for (var i = 0; i < len; i++){
            if (qq.hasClass(candidates[i], className)){
                result.push(candidates[i]);
            }
        }
        return result;
    };

    /**
     * obj2url() takes a json-object as argument and generates
     * a querystring. pretty much like jQuery.param()
     * 
     * how to use:
     *
     *    `qq.obj2url({a:'b',c:'d'},'http://any.url/upload?otherParam=value');`
     *
     * will result in:
     *
     *    `http://any.url/upload?otherParam=value&a=b&c=d`
     *
     * @param  Object JSON-Object
     * @param  String current querystring-part
     * @return String encoded querystring
     */
    qq.obj2url = function(obj, temp, prefixDone){
        var uristrings = [],
            prefix = '&',
            add = function(nextObj, i){
                var nextTemp = temp 
                    ? (/\[\]$/.test(temp)) // prevent double-encoding
                       ? temp
                       : temp+'['+i+']'
                    : i;
                if ((nextTemp != 'undefined') && (i != 'undefined')) {  
                    uristrings.push(
                        (typeof nextObj === 'object') 
                            ? qq.obj2url(nextObj, nextTemp, true)
                            : (Object.prototype.toString.call(nextObj) === '[object Function]')
                                ? encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj())
                                : encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj)                                                          
                    );
                }
            }; 

        if (!prefixDone && temp) {
          prefix = (/\?/.test(temp)) ? (/\?$/.test(temp)) ? '' : '&' : '?';
          uristrings.push(temp);
          uristrings.push(qq.obj2url(obj));
        } else if ((Object.prototype.toString.call(obj) === '[object Array]') && (typeof obj != 'undefined') ) {
            // we wont use a for-in-loop on an array (performance)
            for (var i = 0, len = obj.length; i < len; ++i){
                add(obj[i], i);
            }
        } else if ((typeof obj != 'undefined') && (obj !== null) && (typeof obj === "object")){
            // for anything else but a scalar, we will use for-in-loop
            for (var i in obj){
                add(obj[i], i);
            }
        } else {
            uristrings.push(encodeURIComponent(temp) + '=' + encodeURIComponent(obj));
        }

        return uristrings.join(prefix)
                         .replace(/^&/, '')
                         .replace(/%20/g, '+'); 
    };

    //
    //
    // Uploader Classes
    //
    //

    var qq = qq || {};
        
    /**
     * Creates upload button, validates upload, but doesn't create file list or dd. 
     */
    qq.FileUploaderBasic = function(o){
        this._options = {
            // set to true to see the server response
            debug: false,
            action: '/server/upload',
            params: {},
            button: null,
            multiple: true,
            maxConnections: 3,
            // validation        
            allowedExtensions: [],               
            sizeLimit: 0,   
            minSizeLimit: 0,                             
            // events
            // return false to cancel submit
            onSubmit: function(id, fileName){},
            onProgress: function(id, fileName, loaded, total){},
            onComplete: function(id, fileName, responseJSON){},
            onCancel: function(id, fileName){},
            // messages                
            messages: {
                typeError: "{file} has invalid extension. Only {extensions} are allowed.",
                sizeError: "{file} is too large, maximum file size is {sizeLimit}.",
                minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.",
                emptyError: "{file} is empty, please select files again without it.",
                onLeave: "The files are being uploaded, if you leave now the upload will be cancelled."            
            },
            showMessage: function(message){
                alert(message);
            }               
        };
        qq.extend(this._options, o);
            
        // number of files being uploaded
        this._filesInProgress = 0;
        this._handler = this._createUploadHandler(); 
        
        if (this._options.button){ 
            this._button = this._createUploadButton(this._options.button);
        }
                            
        this._preventLeaveInProgress();         
    };
       
    qq.FileUploaderBasic.prototype = {
        setParams: function(params){
            this._options.params = params;
        },
        getInProgress: function(){
            return this._filesInProgress;         
        },
        _createUploadButton: function(element){
            var self = this;
            
            return new qq.UploadButton({
                element: element,
                multiple: this._options.multiple && qq.UploadHandlerXhr.isSupported(),
                onChange: function(input){
                    self._onInputChange(input);
                }        
            });           
        },    
        _createUploadHandler: function(){
            var self = this,
                handlerClass;        
            
            if(qq.UploadHandlerXhr.isSupported()){           
                handlerClass = 'UploadHandlerXhr';                        
            } else {
                handlerClass = 'UploadHandlerForm';
            }

            var handler = new qq[handlerClass]({
                debug: this._options.debug,
                action: this._options.action,         
                maxConnections: this._options.maxConnections,   
                onProgress: function(id, fileName, loaded, total){                
                    self._onProgress(id, fileName, loaded, total);
                    self._options.onProgress(id, fileName, loaded, total);                    
                },            
                onComplete: function(id, fileName, result){
                    self._onComplete(id, fileName, result);
                    self._options.onComplete(id, fileName, result);
                },
                onCancel: function(id, fileName){
                    self._onCancel(id, fileName);
                    self._options.onCancel(id, fileName);
                }
            });

            return handler;
        },    
        _preventLeaveInProgress: function(){
            var self = this;
            
            qq.attach(window, 'beforeunload', function(e){
                if (!self._filesInProgress){return;}
                
                var e = e || window.event;
                // for ie, ff
                e.returnValue = self._options.messages.onLeave;
                // for webkit
                return self._options.messages.onLeave;             
            });        
        },    
        _onSubmit: function(id, fileName){
            this._filesInProgress++;  
        },
        _onProgress: function(id, fileName, loaded, total){        
        },
        _onComplete: function(id, fileName, result){
            this._filesInProgress--;                 
            if (result.error){
                this._options.showMessage(result.error);
            }             
        },
        _onCancel: function(id, fileName){
            this._filesInProgress--;        
        },
        _onInputChange: function(input){
            if (this._handler instanceof qq.UploadHandlerXhr){                
                this._uploadFileList(input.files);                   
            } else {             
                if (this._validateFile(input)){                
                    this._uploadFile(input);                                    
                }                      
            }               
            this._button.reset();   
        },  
        _uploadFileList: function(files){
            for (var i=0; i<files.length; i++){
                if ( !this._validateFile(files[i])){
                    return;
                }            
            }
            
            for (var i=0; i<files.length; i++){
                this._uploadFile(files[i]);        
            }        
        },       
        _uploadFile: function(fileContainer){      
            var id = this._handler.add(fileContainer);
            var fileName = this._handler.getName(id);
            
            if (this._options.onSubmit(id, fileName) !== false){
                this._onSubmit(id, fileName);
                this._handler.upload(id, this._options.params);
            }
        },      
        _validateFile: function(file){
            var name, size;
            
            if (file.value){
                // it is a file input            
                // get input value and remove path to normalize
                name = file.value.replace(/.*(\/|\\)/, "");
            } else {
                // fix missing properties in Safari
                name = file.fileName != null ? file.fileName : file.name;
                size = file.fileSize != null ? file.fileSize : file.size;
            }
                        
            if (! this._isAllowedExtension(name)){            
                this._error('typeError', name);
                return false;
                
            } else if (size === 0){            
                this._error('emptyError', name);
                return false;
                                                         
            } else if (size && this._options.sizeLimit && size > this._options.sizeLimit){            
                this._error('sizeError', name);
                return false;
                            
            } else if (size && size < this._options.minSizeLimit){
                this._error('minSizeError', name);
                return false;            
            }
            
            return true;                
        },
        _error: function(code, fileName){
            var message = this._options.messages[code];        
            function r(name, replacement){ message = message.replace(name, replacement); }
            
            r('{file}', this._formatFileName(fileName));        
            r('{extensions}', this._options.allowedExtensions.join(', '));
            r('{sizeLimit}', this._formatSize(this._options.sizeLimit));
            r('{minSizeLimit}', this._formatSize(this._options.minSizeLimit));
            
            this._options.showMessage(message);                
        },
        _formatFileName: function(name){
            if (name.length > 33){
                name = name.slice(0, 19) + '...' + name.slice(-13);    
            }
            return name;
        },
        _isAllowedExtension: function(fileName){
            var ext = (-1 !== fileName.indexOf('.')) ? fileName.replace(/.*[.]/, '').toLowerCase() : '';
            var allowed = this._options.allowedExtensions;
            
            if (!allowed.length){return true;}        
            
            for (var i=0; i<allowed.length; i++){
                if (allowed[i].toLowerCase() == ext){ return true;}    
            }
            
            return false;
        },    
        _formatSize: function(bytes){
            var i = -1;                                    
            do {
                bytes = bytes / 1024;
                i++;  
            } while (bytes > 99);
            
            return Math.max(bytes, 0.1).toFixed(1) + ['kB', 'MB', 'GB', 'TB', 'PB', 'EB'][i];          
        }
    };
        
           
    /**
     * Class that creates upload widget with drag-and-drop and file list
     * @inherits qq.FileUploaderBasic
     */
    qq.FileUploader = function(o){
        // call parent constructor
        qq.FileUploaderBasic.apply(this, arguments);
        
        // additional options    
        qq.extend(this._options, {
            element: null,
            // if set, will be used instead of qq-upload-list in template
            listElement: null,
                    
            template: '<div class="qq-uploader">' + 
                    '<div class="qq-upload-drop-area"><span>Drop files here to upload</span></div>' +
                    '<div class="qq-upload-button">Upload a file</div>' +
                    '<ul class="qq-upload-list"></ul>' + 
                 '</div>',

            // template for one item in file list
            fileTemplate: '<li>' +
                    '<span class="qq-upload-file"></span>' +
                    '<span class="qq-upload-spinner"></span>' +
                    '<span class="qq-upload-size"></span>' +
                    '<a class="qq-upload-cancel" href="#">Cancel</a>' +
                    '<span class="qq-upload-failed-text">Failed</span>' +
                '</li>',        
            
            classes: {
                // used to get elements from templates
                button: 'qq-upload-button',
                drop: 'qq-upload-drop-area',
                dropActive: 'qq-upload-drop-area-active',
                list: 'qq-upload-list',
                            
                file: 'qq-upload-file',
                spinner: 'qq-upload-spinner',
                size: 'qq-upload-size',
                cancel: 'qq-upload-cancel',

                // added to list item when upload completes
                // used in css to hide progress spinner
                success: 'qq-upload-success',
                fail: 'qq-upload-fail'
            }
        });
        // overwrite options with user supplied    
        qq.extend(this._options, o);       

        this._element = this._options.element;
        this._element.innerHTML = this._options.template;        
        this._listElement = this._options.listElement || this._find(this._element, 'list');
        
        this._classes = this._options.classes;
            
        this._button = this._createUploadButton(this._find(this._element, 'button'));        
        
        this._bindCancelEvent();
        this._setupDragDrop();
    };

    // inherit from Basic Uploader
    qq.extend(qq.FileUploader.prototype, qq.FileUploaderBasic.prototype);

    qq.extend(qq.FileUploader.prototype, {
        /**
         * Gets one of the elements listed in this._options.classes
         **/
        _find: function(parent, type){                                
            var element = qq.getByClass(parent, this._options.classes[type])[0];        
            if (!element){
                throw new Error('element not found ' + type);
            }
            
            return element;
        },
        _setupDragDrop: function(){
            var self = this,
                dropArea = this._find(this._element, 'drop');                        

            var dz = new qq.UploadDropZone({
                element: dropArea,
                onEnter: function(e){
                    qq.addClass(dropArea, self._classes.dropActive);
                    e.stopPropagation();
                },
                onLeave: function(e){
                    e.stopPropagation();
                },
                onLeaveNotDescendants: function(e){
                    qq.removeClass(dropArea, self._classes.dropActive);  
                },
                onDrop: function(e){
                    dropArea.style.display = 'none';
                    qq.removeClass(dropArea, self._classes.dropActive);
                    self._uploadFileList(e.dataTransfer.files);    
                }
            });
                    
            dropArea.style.display = 'none';

            qq.attach(document, 'dragenter', function(e){     
                if (!dz._isValidFileDrag(e)) return; 
                
                dropArea.style.display = 'block';            
            });                 
            qq.attach(document, 'dragleave', function(e){
                if (!dz._isValidFileDrag(e)) return;            
                
                var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
                // only fire when leaving document out
                if ( ! relatedTarget || relatedTarget.nodeName == "HTML"){               
                    dropArea.style.display = 'none';                                            
                }
            });                
        },
        _onSubmit: function(id, fileName){
            qq.FileUploaderBasic.prototype._onSubmit.apply(this, arguments);
            this._addToList(id, fileName);  
        },
        _onProgress: function(id, fileName, loaded, total){
            qq.FileUploaderBasic.prototype._onProgress.apply(this, arguments);

            var item = this._getItemByFileId(id);
            var size = this._find(item, 'size');
            size.style.display = 'inline';
            
            var text; 
            if (loaded != total){
                text = Math.round(loaded / total * 100) + '% from ' + this._formatSize(total);
            } else {                                   
                text = this._formatSize(total);
            }          
            
            qq.setText(size, text);         
        },
        _onComplete: function(id, fileName, result){
            qq.FileUploaderBasic.prototype._onComplete.apply(this, arguments);

            // mark completed
            var item = this._getItemByFileId(id);                
            qq.remove(this._find(item, 'cancel'));
            qq.remove(this._find(item, 'spinner'));
            
            if (result.success){
                qq.addClass(item, this._classes.success);    
            } else {
                qq.addClass(item, this._classes.fail);
            }         
        },
        _addToList: function(id, fileName){
            var item = qq.toElement(this._options.fileTemplate);                
            item.qqFileId = id;

            var fileElement = this._find(item, 'file');        
            qq.setText(fileElement, this._formatFileName(fileName));
            this._find(item, 'size').style.display = 'none';        

            this._listElement.appendChild(item);
        },
        _getItemByFileId: function(id){
            var item = this._listElement.firstChild;        
            
            // there can't be txt nodes in dynamically created list
            // and we can  use nextSibling
            while (item){            
                if (item.qqFileId == id) return item;            
                item = item.nextSibling;
            }          
        },
        /**
         * delegate click event for cancel link 
         **/
        _bindCancelEvent: function(){
            var self = this,
                list = this._listElement;            
            
            qq.attach(list, 'click', function(e){            
                e = e || window.event;
                var target = e.target || e.srcElement;
                
                if (qq.hasClass(target, self._classes.cancel)){                
                    qq.preventDefault(e);
                   
                    var item = target.parentNode;
                    self._handler.cancel(item.qqFileId);
                    qq.remove(item);
                }
            });
        }    
    });
        
    qq.UploadDropZone = function(o){
        this._options = {
            element: null,  
            onEnter: function(e){},
            onLeave: function(e){},  
            // is not fired when leaving element by hovering descendants   
            onLeaveNotDescendants: function(e){},   
            onDrop: function(e){}                       
        };
        qq.extend(this._options, o); 
        
        this._element = this._options.element;
        
        this._disableDropOutside();
        this._attachEvents();   
    };

    qq.UploadDropZone.prototype = {
        _disableDropOutside: function(e){
            // run only once for all instances
            if (!qq.UploadDropZone.dropOutsideDisabled ){

                qq.attach(document, 'dragover', function(e){
                    if (e.dataTransfer){
                        e.dataTransfer.dropEffect = 'none';
                        e.preventDefault(); 
                    }           
                });
                
                qq.UploadDropZone.dropOutsideDisabled = true; 
            }        
        },
        _attachEvents: function(){
            var self = this;              
                      
            qq.attach(self._element, 'dragover', function(e){
                if (!self._isValidFileDrag(e)) return;
                
                var effect = e.dataTransfer.effectAllowed;
                if (effect == 'move' || effect == 'linkMove'){
                    e.dataTransfer.dropEffect = 'move'; // for FF (only move allowed)    
                } else {                    
                    e.dataTransfer.dropEffect = 'copy'; // for Chrome
                }
                                                         
                e.stopPropagation();
                e.preventDefault();                                                                    
            });
            
            qq.attach(self._element, 'dragenter', function(e){
                if (!self._isValidFileDrag(e)) return;
                            
                self._options.onEnter(e);
            });
            
            qq.attach(self._element, 'dragleave', function(e){
                if (!self._isValidFileDrag(e)) return;
                
                self._options.onLeave(e);
                
                var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);                      
                // do not fire when moving a mouse over a descendant
                if (qq.contains(this, relatedTarget)) return;
                            
                self._options.onLeaveNotDescendants(e); 
            });
                    
            qq.attach(self._element, 'drop', function(e){
                if (!self._isValidFileDrag(e)) return;
                
                e.preventDefault();
                self._options.onDrop(e);
            });          
        },
        _isValidFileDrag: function(e){
            var dt = e.dataTransfer,
                // do not check dt.types.contains in webkit, because it crashes safari 4            
                isWebkit = navigator.userAgent.indexOf("AppleWebKit") > -1;                        

            // dt.effectAllowed is none in Safari 5
            // dt.types.contains check is for firefox            
            return dt && dt.effectAllowed != 'none' && 
                (dt.files || (!isWebkit && dt.types.contains && dt.types.contains('Files')));
            
        }        
    }; 

    qq.UploadButton = function(o){
        this._options = {
            element: null,  
            // if set to true adds multiple attribute to file input      
            multiple: false,
            // name attribute of file input
            name: 'file',
            onChange: function(input){},
            hoverClass: 'qq-upload-button-hover',
            focusClass: 'qq-upload-button-focus'                       
        };
        
        qq.extend(this._options, o);
            
        this._element = this._options.element;
        
        // make button suitable container for input
        qq.css(this._element, {
            position: 'relative',
            overflow: 'hidden',
            // Make sure browse button is in the right side
            // in Internet Explorer
            direction: 'ltr'
        });   
        
        this._input = this._createInput();
    };

    qq.UploadButton.prototype = {
        /* returns file input element */    
        getInput: function(){
            return this._input;
        },
        /* cleans/recreates the file input */
        reset: function(){
            if (this._input.parentNode){
                qq.remove(this._input);    
            }                
            
            qq.removeClass(this._element, this._options.focusClass);
            this._input = this._createInput();
        },    
        _createInput: function(){                
            var input = document.createElement("input");
            
            if (this._options.multiple){
                input.setAttribute("multiple", "multiple");
            }
                    
            input.setAttribute("type", "file");
            input.setAttribute("name", this._options.name);
            
            qq.css(input, {
                position: 'absolute',
                // in Opera only 'browse' button
                // is clickable and it is located at
                // the right side of the input
                right: 0,
                top: 0,
                fontFamily: 'Arial',
                // 4 persons reported this, the max values that worked for them were 243, 236, 236, 118
                fontSize: '118px',
                margin: 0,
                padding: 0,
                cursor: 'pointer',
                opacity: 0
            });
            
            this._element.appendChild(input);

            var self = this;
            qq.attach(input, 'change', function(){
                self._options.onChange(input);
            });
                    
            qq.attach(input, 'mouseover', function(){
                qq.addClass(self._element, self._options.hoverClass);
            });
            qq.attach(input, 'mouseout', function(){
                qq.removeClass(self._element, self._options.hoverClass);
            });
            qq.attach(input, 'focus', function(){
                qq.addClass(self._element, self._options.focusClass);
            });
            qq.attach(input, 'blur', function(){
                qq.removeClass(self._element, self._options.focusClass);
            });

            // IE and Opera, unfortunately have 2 tab stops on file input
            // which is unacceptable in our case, disable keyboard access
            if (window.attachEvent){
                // it is IE or Opera
                input.setAttribute('tabIndex', "-1");
            }

            return input;            
        }        
    };

    /**
     * Class for uploading files, uploading itself is handled by child classes
     */
    qq.UploadHandlerAbstract = function(o){
        this._options = {
            debug: false,
            action: '/upload.php',
            // maximum number of concurrent uploads        
            maxConnections: 999,
            onProgress: function(id, fileName, loaded, total){},
            onComplete: function(id, fileName, response){},
            onCancel: function(id, fileName){}
        };
        qq.extend(this._options, o);    
        
        this._queue = [];
        // params for files in queue
        this._params = [];
    };
    qq.UploadHandlerAbstract.prototype = {
        log: function(str){
            if (this._options.debug && window.console) console.log('[uploader] ' + str);        
        },
        /**
         * Adds file or file input to the queue
         * @returns id
         **/    
        add: function(file){},
        /**
         * Sends the file identified by id and additional query params to the server
         */
        upload: function(id, params){
            var len = this._queue.push(id);

            var copy = {};        
            qq.extend(copy, params);
            this._params[id] = copy;        
                    
            // if too many active uploads, wait...
            if (len <= this._options.maxConnections){               
                this._upload(id, this._params[id]);
            }
        },
        /**
         * Cancels file upload by id
         */
        cancel: function(id){
            this._cancel(id);
            this._dequeue(id);
        },
        /**
         * Cancells all uploads
         */
        cancelAll: function(){
            for (var i=0; i<this._queue.length; i++){
                this._cancel(this._queue[i]);
            }
            this._queue = [];
        },
        /**
         * Returns name of the file identified by id
         */
        getName: function(id){},
        /**
         * Returns size of the file identified by id
         */          
        getSize: function(id){},
        /**
         * Returns id of files being uploaded or
         * waiting for their turn
         */
        getQueue: function(){
            return this._queue;
        },
        /**
         * Actual upload method
         */
        _upload: function(id){},
        /**
         * Actual cancel method
         */
        _cancel: function(id){},     
        /**
         * Removes element from queue, starts upload of next
         */
        _dequeue: function(id){
            var i = qq.indexOf(this._queue, id);
            this._queue.splice(i, 1);
                    
            var max = this._options.maxConnections;
            
            if (this._queue.length >= max && i < max){
                var nextId = this._queue[max-1];
                this._upload(nextId, this._params[nextId]);
            }
        }        
    };

    /**
     * Class for uploading files using form and iframe
     * @inherits qq.UploadHandlerAbstract
     */
    qq.UploadHandlerForm = function(o){
        qq.UploadHandlerAbstract.apply(this, arguments);
           
        this._inputs = {};
    };
    // @inherits qq.UploadHandlerAbstract
    qq.extend(qq.UploadHandlerForm.prototype, qq.UploadHandlerAbstract.prototype);

    qq.extend(qq.UploadHandlerForm.prototype, {
        add: function(fileInput){
            fileInput.setAttribute('name', 'qqfile');
            var id = 'qq-upload-handler-iframe' + qq.getUniqueId();       
            
            this._inputs[id] = fileInput;
            
            // remove file input from DOM
            if (fileInput.parentNode){
                qq.remove(fileInput);
            }
                    
            return id;
        },
        getName: function(id){
            // get input value and remove path to normalize
            return this._inputs[id].value.replace(/.*(\/|\\)/, "");
        },    
        _cancel: function(id){
            this._options.onCancel(id, this.getName(id));
            
            delete this._inputs[id];        

            var iframe = document.getElementById(id);
            if (iframe){
                // to cancel request set src to something else
                // we use src="javascript:false;" because it doesn't
                // trigger ie6 prompt on https
                iframe.setAttribute('src', 'javascript:false;');

                qq.remove(iframe);
            }
        },     
        _upload: function(id, params){                        
            var input = this._inputs[id];
            
            if (!input){
                throw new Error('file with passed id was not added, or already uploaded or cancelled');
            }                

            var fileName = this.getName(id);
                    
            var iframe = this._createIframe(id);
            var form = this._createForm(iframe, params);
            form.appendChild(input);

            var self = this;
            this._attachLoadEvent(iframe, function(){                                 
                self.log('iframe loaded');
                
                var response = self._getIframeContentJSON(iframe);

                self._options.onComplete(id, fileName, response);
                self._dequeue(id);
                
                delete self._inputs[id];
                // timeout added to fix busy state in FF3.6
                setTimeout(function(){
                    qq.remove(iframe);
                }, 1);
            });

            form.submit();        
            qq.remove(form);        
            
            return id;
        }, 
        _attachLoadEvent: function(iframe, callback){
            qq.attach(iframe, 'load', function(){
                // when we remove iframe from dom
                // the request stops, but in IE load
                // event fires
                if (!iframe.parentNode){
                    return;
                }

                // fixing Opera 10.53
                if (iframe.contentDocument &&
                    iframe.contentDocument.body &&
                    iframe.contentDocument.body.innerHTML == "false"){
                    // In Opera event is fired second time
                    // when body.innerHTML changed from false
                    // to server response approx. after 1 sec
                    // when we upload file with iframe
                    return;
                }

                callback();
            });
        },
        /**
         * Returns json object received by iframe from server.
         */
        _getIframeContentJSON: function(iframe){
            // iframe.contentWindow.document - for IE<7
            var doc = iframe.contentDocument ? iframe.contentDocument: iframe.contentWindow.document,
                response;
            
            this.log("converting iframe's innerHTML to JSON");
            this.log("innerHTML = " + doc.body.innerHTML);
                            
            try {
                response = eval("(" + doc.body.innerHTML + ")");
            } catch(err){
                response = {};
            }        

            return response;
        },
        /**
         * Creates iframe with unique name
         */
        _createIframe: function(id){
            // We can't use following code as the name attribute
            // won't be properly registered in IE6, and new window
            // on form submit will open
            // var iframe = document.createElement('iframe');
            // iframe.setAttribute('name', id);

            var iframe = qq.toElement('<iframe src="javascript:false;" name="' + id + '" />');
            // src="javascript:false;" removes ie6 prompt on https

            iframe.setAttribute('id', id);

            iframe.style.display = 'none';
            document.body.appendChild(iframe);

            return iframe;
        },
        /**
         * Creates form, that will be submitted to iframe
         */
        _createForm: function(iframe, params){
            // We can't use the following code in IE6
            // var form = document.createElement('form');
            // form.setAttribute('method', 'post');
            // form.setAttribute('enctype', 'multipart/form-data');
            // Because in this case file won't be attached to request
            var form = qq.toElement('<form method="post" enctype="multipart/form-data"></form>');

            var queryString = qq.obj2url(params, this._options.action);

            form.setAttribute('action', queryString);
            form.setAttribute('target', iframe.name);
            form.style.display = 'none';
            document.body.appendChild(form);

            return form;
        }
    });

    /**
     * Class for uploading files using xhr
     * @inherits qq.UploadHandlerAbstract
     */
    qq.UploadHandlerXhr = function(o){
        qq.UploadHandlerAbstract.apply(this, arguments);

        this._files = [];
        this._xhrs = [];
        
        // current loaded size in bytes for each file 
        this._loaded = [];
    };

    // static method
    qq.UploadHandlerXhr.isSupported = function(){
        var input = document.createElement('input');
        input.type = 'file';        
        
        return (
            'multiple' in input &&
            typeof File != "undefined" &&
            typeof (new XMLHttpRequest()).upload != "undefined" );       
    };

    // @inherits qq.UploadHandlerAbstract
    qq.extend(qq.UploadHandlerXhr.prototype, qq.UploadHandlerAbstract.prototype)

    qq.extend(qq.UploadHandlerXhr.prototype, {
        /**
         * Adds file to the queue
         * Returns id to use with upload, cancel
         **/    
        add: function(file){
            if (!(file instanceof File)){
                throw new Error('Passed obj in not a File (in qq.UploadHandlerXhr)');
            }
                    
            return this._files.push(file) - 1;        
        },
        getName: function(id){        
            var file = this._files[id];
            // fix missing name in Safari 4
            return file.fileName != null ? file.fileName : file.name;       
        },
        getSize: function(id){
            var file = this._files[id];
            return file.fileSize != null ? file.fileSize : file.size;
        },    
        /**
         * Returns uploaded bytes for file identified by id 
         */    
        getLoaded: function(id){
            return this._loaded[id] || 0; 
        },
        /**
         * Sends the file identified by id and additional query params to the server
         * @param {Object} params name-value string pairs
         */    
        _upload: function(id, params){
            var file = this._files[id],
                name = this.getName(id),
                size = this.getSize(id);
                    
            this._loaded[id] = 0;
                                    
            var xhr = this._xhrs[id] = new XMLHttpRequest();
            var self = this;
                                            
            xhr.upload.onprogress = function(e){
                if (e.lengthComputable){
                    self._loaded[id] = e.loaded;
                    self._options.onProgress(id, name, e.loaded, e.total);
                }
            };

            xhr.onreadystatechange = function(){            
                if (xhr.readyState == 4){
                    self._onComplete(id, xhr);                    
                }
            };

            // build query string
            params = params || {};
            params['qqfile'] = name;
            var queryString = qq.obj2url(params, this._options.action);

            xhr.open("POST", queryString, true);
            xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
            xhr.setRequestHeader("X-File-Name", encodeURIComponent(name));
            xhr.setRequestHeader("Content-Type", "application/octet-stream");
            xhr.send(file);
        },
        _onComplete: function(id, xhr){
            // the request was aborted/cancelled
            if (!this._files[id]) return;
            
            var name = this.getName(id);
            var size = this.getSize(id);
            
            this._options.onProgress(id, name, size, size);
                    
            if (xhr.status == 200){
                this.log("xhr - server response received");
                this.log("responseText = " + xhr.responseText);
                            
                var response;
                        
                try {
                    response = eval("(" + xhr.responseText + ")");
                } catch(err){
                    response = {};
                }
                
                this._options.onComplete(id, name, response);
                            
            } else {                   
                this._options.onComplete(id, name, {});
            }
                    
            this._files[id] = null;
            this._xhrs[id] = null;    
            this._dequeue(id);                    
        },
        _cancel: function(id){
            this._options.onCancel(id, this.getName(id));
            
            this._files[id] = null;
            
            if (this._xhrs[id]){
                this._xhrs[id].abort();
                this._xhrs[id] = null;                                   
            }
        }
    });

    return qq;
});